/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.copier.engine;

import java.util.*;
import java.util.Map.*;
import ch.trackedbean.copier.*;
import ch.trackedbean.copier.annotations.*;

/**
 * Mapping engine.<br>
 * The mapper is thread safe after the configuration has been done.
 * 
 * @author M. Hautle
 */
public class BeanMapper implements IBeanMapper {
	/** The base class specified in {@link SourceClass} or null. */
	private final Class mappedBaseType;

	/** The destination class. */
	private final Class dstType;

	/** The mappings in this mapper. Key = Base class on which the mappings get applied. */
	private final Map<Class, List<IMappingHolder>> mappings = new HashMap<Class, List<IMappingHolder>>();

	/** Destination property to {@link IMappingHolder}s mapping. */
	private final Map<String, IMappingHolder> dstMappingInfos = new HashMap<String, IMappingHolder>();

	/**
	 * Default constructor.
	 * 
	 * @param mappedBaseType The mapped type (from which to copy the data, may be null)
	 * @param dstType The destination type (to which to copy the data)
	 */
	public BeanMapper(Class mappedBaseType, Class dstType) {
		this.mappedBaseType = mappedBaseType;
		this.dstType = dstType;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean doesSomething() {
		return mappings.size() > 0;
	}

	/**
	 * {@inheritDoc}
	 */
	public Class getMappedBaseType() {
		return mappedBaseType;
	}

	/**
	 * {@inheritDoc}
	 */
	public Class getDstType() {
		return dstType;
	}

	/**
	 * {@inheritDoc}
	 */
	public <T> T copySrc2Dst(Object src, T dst, IMappingContext context) {
		// store relation for cycle detection
		context.putElement(src, dst);
		// process the whole type hierarchy of the passed source object
		for (Class<?> type = src.getClass(); type != null; type = type.getSuperclass()) {
			final List<IMappingHolder> mappings = this.mappings.get(type);
			if (mappings == null)
				continue;
			for (IMappingHolder m : mappings) {
				if (context.copySrc2Dst(m))
					m.copySrc2Dst(src, dst, context);
			}
		}
		return dst;
	}

	/**
	 * {@inheritDoc}
	 */
	public <T> T copyDst2Src(Object dst, T src, IMappingContext context) {
		// store relation for cycle detection
		context.putElement(dst, src);
		// process the whole type hierarchy of the passed source object
		for (Class<?> type = src.getClass(); type != null; type = type.getSuperclass()) {
			final List<IMappingHolder> mappings = this.mappings.get(type);
			if (mappings == null)
				continue;
			for (IMappingHolder m : mappings) {
				if (context.copyDst2Src(m))
					m.copyDst2Src(src, dst, context);
			}
		}
		return src;
	}

	/**
	 * {@inheritDoc}
	 */
	public IMappingHolder getMappingHolder(String property) {
		return dstMappingInfos.get(property);
	}

	/**
	 * {@inheritDoc}
	 */
	public List<IMappingHolder> getMappingHolders(boolean includeInherited) {
		if (mappedBaseType == null)
			return Collections.emptyList();
		return getMappingHolders(mappedBaseType, includeInherited);
	}

	/**
	 * {@inheritDoc}
	 */
	public List<IMappingHolder> getMappingHolders(Class baseClass, boolean includeInherited) {
		if (!includeInherited)
			return getMappingHolders(baseClass);
		final List<IMappingHolder> props = new ArrayList<IMappingHolder>();
		// process the whole type hierarchy of the passed base class
		for (Class<?> type = baseClass; type != null; type = type.getSuperclass()) {
			final List<IMappingHolder> mappings = this.mappings.get(type);
			if (mappings != null)
				props.addAll(mappings);
		}
		return props;
	}

	/**
	 * {@inheritDoc}
	 */
	public List<IMappingHolder> getMappingHolders(Class baseClass) {
		final List<IMappingHolder> mappings = this.mappings.get(baseClass);
		if (mappings == null)
			return Collections.emptyList();
		return new ArrayList<IMappingHolder>(mappings);
	}

	/**
	 * Adds the given mapping for the given base type.
	 * 
	 * @param type The type on which to register the mapping
	 * @param property The property name concerned by the given mapping
	 * @param mapping The mapping
	 */
	public void addMapping(Class type, String property, IMappingHolder mapping) {
		List<IMappingHolder> list = mappings.get(type);
		if (list == null)
			mappings.put(type, list = new ArrayList<IMappingHolder>());
		list.add(mapping);
		dstMappingInfos.put(property, mapping);
	}

	/**
	 * Appends the mapping information from the given {@link IBeanMapper}.
	 * 
	 * @param mapper The mapper
	 */
	public void addMappings(BeanMapper mapper) {
		for (Entry<Class, List<IMappingHolder>> e : mapper.mappings.entrySet()) {
			final Class<?> key = e.getKey();
			List<IMappingHolder> list = mappings.get(key);
			if (list == null)
				mappings.put(key, list = new ArrayList<IMappingHolder>());
			// add the mapper information
			for (IMappingHolder m : e.getValue()) {
				list.add(m);
				dstMappingInfos.put(m.getDestinationAccessor().getPath(), m);
			}
		}
	}
}
